<!DOCTYPE html>
<html lang="en">
<head>
    <title>基于多模态的3D宠物私人助理</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <link type="text/css" rel="stylesheet" href="main.css">
</head>

<body>
<div id="info">
    小狗助理
</div>

<style>

    * {
        margin: 0;
        padding: 0;
        font-family: 'Montserrat', sans-serif;
        box-sizing: border-box;
        animation-timing-function: ease;
    }

    body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        overflow: hidden;
    }

    #wrapper {
        position: absolute;
        width: 350px;
        animation: out .3s forwards ease;
        animation-delay: 5s;
        transform: scale(1);
        opacity: 1;
        transform-origin: center -80%;


        @keyframes out {
            0% {
                transform: scale(1);
                opacity: 1;
            }
            100% {
                transform: scale(0);
                opacity: 0;
            }
        }


        .loading-bar {
            width: 100%;
            height: 30px;
            background: #dfe6e9;
            border-radius: 5px;

            .progress-bar {
                animation: progress 3.5s forwards;
                width: 0%;
                height: 100%;
                background: #a29bfe;
                border-radius: 5px;
                border: 0 solid #0abde3;

                @keyframes progress {
                    0% {
                        width: 6%;
                    }
                    50% {
                        border: 0 solid #0abde3;
                    }
                    51% {
                        border: 4px solid #0abde3;
                    }
                    85% {
                        width: 11%;
                    }
                    95% {
                        border: 4px solid #0abde3;
                        width: 100%;
                    }
                    100% {
                        width: 100%;
                    }
                }
            }
        }

        .status {
            margin-top: 10px;

            .state {
                float: left;
                font-size: 0.9em;
                letter-spacing: 1pt;
                text-transform: uppercase;
                width: 100px;
                height: 20px;
                position: relative;

                &:before {
                    content: "Loading";
                    position: absolute;
                    left: 0;
                    top: 0;
                    animation: fadeLeft .5s forwards ease;
                    animation-delay: 3.2s;

                    @keyframes fadeLeft {
                        0% {
                            text-indent: 0;
                            opacity: 1;
                        }
                        100% {
                            text-indent: -100px;
                            opacity: 0;
                        }
                    }
                }

                &:after {
                    content: "Complete";
                    position: absolute;
                    left: 0;
                    top: 0;
                    text-indent: 100px;
                    opacity: 0;
                    animation: fadeLeft2 .5s forwards ease;
                    animation-delay: 3.2s;

                    @keyframes fadeLeft2 {
                        0% {
                            text-indent: 100px;
                            opacity: 0;
                        }
                        100% {
                            text-indent: 0;
                            opacity: 1;
                        }
                    }
                }
            }

            .percentage {


                &:before {
                    -webkit-animation: percentage-slow 3s forwards, percentage-fast .4s forwards;
                    -webkit-animation-delay: 0s, 3s;
                    content: attr(data-content);
                    font-size: 0.9em;
                    letter-spacing: 1pt;

                    @keyframes percentage-slow {
                        0% {
                            content: "6%"
                        }
                        25% {
                            content: "7%"
                        }
                        50% {
                            content: "8%"
                        }
                        75% {
                            content: "9%"
                        }
                        100% {
                            content: "10%"
                        }
                    }
                }
            }
        }
    }
</style>

<div id="wrapper">
    <div class="loading-bar">
        <div class="progress-bar"></div>
    </div>
    <div class="status">
        <div class="state"></div>
        <div class="percentage" data-content="0%"></div>
    </div>
</div>

<script type="importmap">
    {
        "imports": {
            "three": "./build/three.module.js",
            "three/addons/": "./jsm/",
            "pako": "./pako/"
        }
    }
</script>

<script type="module">

    import * as THREE from 'three';
    import {TGALoader} from 'three/addons/loaders/TGALoader.js';

    import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
    import {FBXLoader} from 'three/addons/loaders/FBXLoader.js';
    import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
    import {FileLoader} from 'three/addons/loaders/FileLoader.js';

    const manager = new THREE.LoadingManager();
    const tgaLoader = new TGALoader()
    const fbxLoader = new FBXLoader(manager);
    const fileLoader = new FileLoader(manager)

    manager.addHandler(/\.tga$/i, tgaLoader);

    let camera, scene, renderer, stats, objectDog, trackTimes, animationClip, animationAction, objectRoom, dogMaterial;
    let mixerDog;

    const clock = new THREE.Clock();

    const assets = [
        'dog/Corgi Normals.tga',
        'dog/Corgi.tga',
        '../gltf/room/white_sofa_normal.png',
        '../gltf/room/scene.bin',
        '../gltf/room/multicolor_baseColor.png',
        '../gltf/room/scene.gltf',
        //'dog/workroom.fbx',
        'dog/Corgi_All_Animations.fbx'
    ];
    let assetIndex = 0

    const actionFrames = {
        'Corgi_Air_Flip': {'begin': 1905, 'end': 1925},
        'Corgi_Bark': {'begin': 760, 'end': 860},
        'Corgi_Bite': {'begin': 865, 'end': 905},
        'Corgi_Damage': {'begin': 910, 'end': 935},
        'Corgi_Fall': {'begin': 1930, 'end': 1960},
        'Corgi_Give_Paw': {'begin': 940, 'end': 1040},
        'Corgi_Hear_Something': {'begin': 1045, 'end': 1195},
        'Corgi_Idle': {'begin': 0, 'end': 140},
        'Corgi_Idle_Focused': {'begin': 250, 'end': 360},
        'Corgi_Idle_Happy': {'begin': 145, 'end': 246},
        'Corgi_Idle_Lay_Down': {'begin': 480, 'end': 600},
        'Corgi_Idle_Sit': {'begin': 365, 'end': 475},
        'Corgi_Idle_Sleep': {'begin': 605, 'end': 755},
        'Corgi_Jump_Up': {'begin': 1870, 'end': 1900},
        'Corgi_Land': {'begin': 1965, 'end': 1990},
        'Corgi_Run': {'begin': 1715, 'end': 1730},
        'Corgi_Run_In_Place': {'begin': 1825, 'end': 1840},
        'Corgi_Run_Turn_Left': {'begin': 1755, 'end': 1770},
        'Corgi_Run_Turn_Right': {'begin': 1735, 'end': 1750},
        'Corgi_Scratch': {'begin': 1200, 'end': 1290},
        'Corgi_Shake': {'begin': 1295, 'end': 1375},
        'Corgi_Sniff': {'begin': 1380, 'end': 1510},
        'Corgi_Tilt_Head': {'begin': 1515, 'end': 16355},
        'Corgi_Walk': {'begin': 1640, 'end': 1660},
        'Corgi_Walk_Backwards': {'begin': 1775, 'end': 1795},
        'Corgi_Walk_Backwards_In_Place': {'begin': 1845, 'end': 1865},
        'Corgi_Walk_In_Place': {'begin': 1800, 'end': 1820},
        'Corgi_Walk_Turn_Left': {'begin': 1690, 'end': 1710},
        'Corgi_Walk_Turn_Right': {'begin': 1665, 'end': 1685}
    }

    init();
    preload();
    loadDogRoomByGLTF();
    loadNotebook();
    loadPhone();
    function onProgress(event) {
        let percent = Math.floor(((assetIndex) / assets.length) * 100 + (event.loaded / event.total) * (1 / assets.length * 100)).toString() + "%";
        console.log(event.loaded + ', ' + event.total + '  ' + percent);
        document.getElementsByClassName("progress-bar")[0].style.width = percent;
        document.getElementsByClassName("percentage")[0].setAttribute("data-content", percent);
    }

    function onEnd(buff) {
        if (assetIndex < assets.length && assets[assetIndex].indexOf('Corgi_All_Animations') >= 0) {
            document.getElementById("wrapper").remove();
            console.log('load corgi animation');
            [objectDog, mixerDog] = loadAnimation(objectDog, buff);
            objectDog.position.y += 0.1;
            objectDog.position.z += 0.5;

        } else if (assetIndex < assets.length && assets[assetIndex].indexOf('workroom') >= 0) {
            console.log('load corgi room');
            [objectRoom] = loadAnimation(objectRoom, buff)
        }
        assetIndex++
        preload()
    }

    function preload() {
        if (assetIndex >= assets.length) {
            return;
        }
        let asset = assets[assetIndex]
        console.log('loading ' + asset + ' ,' + assetIndex);
        if (asset.endsWith('.tga') || asset.endsWith('.tga.gz')) {
            tgaLoader.load('models/fbx/' + asset, onEnd, onProgress, undefined);
        } else if (asset.endsWith('.fbx')) {
            fbxLoader.load('models/fbx/' + asset, onEnd, onProgress, undefined);
        }else
        {
            fileLoader.load('models/fbx/' + asset, onEnd, onProgress, undefined);
        }
    }

    function loadDogRoomByGLTF() {
        const loader = new GLTFLoader().setPath('models/gltf/room/');
        loader.load('scene.gltf', async function (gltf) {

            const model = gltf.scene;

            // wait until the model can be added to the scene without blocking due to shader compilation

            await renderer.compileAsync(model, camera, scene);

            console.log('room position')
            console.log(model.position)
            //model.position.set (0, 120 ,300 )
            scene.add(model);

        });
    }

    function loadNotebook() {
        const loader = new GLTFLoader().setPath('models/gltf/notebook/');
        loader.load('scene.gltf', async function (gltf) {

            const model = gltf.scene;

            // wait until the model can be added to the scene without blocking due to shader compilation

            await renderer.compileAsync(model, camera, scene);

            console.log('notebook position')

            model.scale.set (0.01, 0.01 ,0.01 );
            model.position.y += 0.3;
            model.position.z += 2;
            model.rotation.y = Math.PI / 180 * 135;
            model.rotation.x = Math.PI / 180 * -10;
            //model.rotateY(Math.PI / 180 * 135);
            //model.rotateZ(Math.PI / 180 * 30);
            console.log(model.position)
            scene.add(model);

        });
    }

    function loadPhone(){
        const loader = new GLTFLoader().setPath('models/gltf/phone/');
        loader.load('scene.gltf', async function (gltf) {

            const model = gltf.scene;

            // wait until the model can be added to the scene without blocking due to shader compilation

            await renderer.compileAsync(model, camera, scene);

            console.log('phone position')

            model.scale.set (0.2, 0.2 ,0.2 );
            model.position.x -= 1.3;
            model.position.z += 1.5;
            model.rotation.y = Math.PI / 180 * 90;
            console.log(model.position)
            scene.add(model);

        });
    }
    function init() {

        const container = document.createElement('div');
        document.body.appendChild(container);

        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
        camera.position.set(0, 120, 300);

        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xa0a0a0);
        //scene.fog = new THREE.Fog(0xa0a0a0, 400, 1000);

        const hemiLight = new THREE.AmbientLight(0xffffff, 1);
        hemiLight.position.set(0, 200, 0);
        scene.add(hemiLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 2);
        dirLight.position.set(0, 200, 100);
        dirLight.castShadow = true;
        dirLight.shadow.camera.top = 180;
        dirLight.shadow.camera.bottom = -100;
        dirLight.shadow.camera.left = -120;
        dirLight.shadow.camera.right = 120;
        scene.add(dirLight);

        //scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );

        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setAnimationLoop(animate);
        renderer.shadowMap.enabled = true;

        container.appendChild(renderer.domElement);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', render); // use if there is no animation loop
        controls.minDistance = 2;
        controls.maxDistance = 10;
        controls.target.set(0, 0, -0.2);
        controls.update();


        window.addEventListener('resize', onWindowResize);

        try {
            system.onmessage = function (pose) {
                playPose(pose)
            }
        } catch (error) {
        }
    }

    function loadAnimation(object, group) {
        let mixer = null;
        if (object) {

            object.traverse(function (child) {

                if (child.material) {

                    const materials = Array.isArray(child.material) ? child.material : [child.material];
                    materials.forEach(material => {

                        if (material.map) material.map.dispose();
                        material.dispose();

                    });

                }

                if (child.geometry) child.geometry.dispose();

            });

            scene.remove(object);

        }

        //

        object = group;
        if (object.animations && object.animations.length) {
            trackTimes = object.animations[0].tracks[0].times
            animationClip = object.animations[0]

            mixer = new THREE.AnimationMixer(object);
            animationAction = mixer.clipAction(animationClip);

            mixer.addEventListener('finished', function () {
                // 当动画循环停止时，如果所有的动画都已经播放完毕，则调用animationEnded函数
                console.log('animation finished');
                playPose('Corgi_Idle');
            });


            playPose('Corgi_Idle');
            animationAction.play();
        } else {
            mixer = null;
        }

        //const SHADER_MTL = new THREE.MeshPhysicalMaterial( { color: 0x575757, shininess: 10 } );
        object.traverse(function (child) {

            if (child.isMesh) {
                /*
                dogMaterial = new THREE.ShaderMaterial({
                    vertexShader: THREE.ShaderLib.phong.vertexShader,//document.querySelector('#post-vert').textContent.trim(),
                    fragmentShader: THREE.ShaderLib.phong.fragmentShader,//document.querySelector('#post-frag').textContent.trim(),
                    //uniforms: THREE.ShaderLib.phong.uniforms,
                    //uniforms: {
                    //    dogSkin: {value: child.material.map},
                    //}
                });

                //dogMaterial = new THREE.ShaderMaterial(THREE.ShaderLib.phong);
                console.log( THREE.ShaderLib.phong.vertexShader)
                 */
                //child.material = dogMaterial;
                child.castShadow = true;
                child.receiveShadow = true;
            }

        });
        object.scale.set(0.02, 0.02, 0.02);

        scene.add(object);
        return [object, mixer];
    }

    function playPose(pose) {
        animationAction.reset();
        animationAction.time = trackTimes[actionFrames[pose].begin]
        animationAction.playBeginTime = animationAction.time
        animationAction.playEndTime = animationAction.time + trackTimes[actionFrames[pose].end] - trackTimes[actionFrames[pose].begin];
        console.log(pose + ',' + animationAction.playBeginTime + ',' + animationAction.playEndTime)
        animationAction.loop = THREE.LoopOnce;
        animationAction.clampWhenFinished = true;
        if (pose === 'Corgi_Idle') {
            animationAction.loop = THREE.LoopRepeat
            animationAction.clampWhenFinished = false;
        }
    }

    function onWindowResize() {

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);

    }

    //
    function render() {

        renderer.render(scene, camera);

    }

    function animate() {

        const delta = clock.getDelta();

        if (mixerDog) {
            mixerDog.update(delta);
        }

        renderer.render(scene, camera);

        //stats.update();
        //if (mixer)
        //console.log(mixer.getRoot().position);
    }

</script>

</body>
</html>
